前端工程化开发

发表于 2022-01-19 15144 字 76 min read

文章目录
基础出发,玩转 NPM 包管理与异步编程(Promise、事件循环)喵!接着深入探讨了 Yeoman 脚手架与 Gulp 自动化构建流程,最后带你硬核攻克 Webpack 打包核心、模块化规范(ESM/CommonJS)以及各种性能优化技巧。是一份超全的前端工程化进阶大礼包,帮你从零构建高效的开发体系,让项目部署变得超级轻松喵~!

前端工程化开发

node.js基础

node.js概述

node.js不是一门编程语言,它是一个执行JavaScript代码的工具。
工具是指可以安装在计算机操作系统之上的软件。

浏览器和Node.js都内置了JavaScript V8 Engine
它可以将JavaScript代码编译为计算机能够识别的机器码

在内置了JavaScript V8 Engine以后实际上只能执行ECMASciript.
就是语言中的语法部分。

和浏览器不同,在node.js中是没有DOM和BOM的。所以在Node.js中
不能执行和它们相关的代码。比如window.alert();或者document。
dom和bom是浏览器环境中特有的。

在Node.js中,有很多系统级别的API。
比如对操作系统中的文件和文件夹进行操作。
获取操作系统信息,比如内存总量是多少,系统临时目录在哪,
对系统进程操作等等

node.js能做什么

我们通常使用它来构建服务器端应用和创建前端工程化工具。
JavaScript运行在浏览器中我们就叫它客户端JavaScript
JavaScript运行在Node.js中我们就叫它服务端JavaScript

系统环境变量

在开发环境的操作系统中定义NODE_ENV 变量,值为development,
在生产环境的操作系统定义NODE_ENV变量,值为production。
webpack在运行时通过process.env.NODE_ENV获取变量的值,
从而得出当前代码的运行环境是什么。

环境变量PATH:系统环境变量PATH存储的都是应用程序路径。
当要求系统运行某一个应用程序又没有告诉它程序的完整路径时,
此时操作系统会在当前文件夹中查找应用程序,如果查找不到。
就会去系统环境变量PATH中指定的路径中查找。

全局对象

在node.js环境中是没有window的,所以window对象自然是未定义的。
在node.js环境中的全局对象为global,在global对象中会存在一些和
window对象中名字相同且作用相同的地方。

在Node.js环境中声明的变量不会被添加到全局对象中,变量声明后
只能在当前文件中使用。

global对象中会存在一些和window对象中名字相同作用相同的方法。
global.console.log();
global.setinterval();
global.clearinterval();
global.setTimeout();
global.clearTimeout();
global.setinmmediate();

在Node.js环境中声明的变量不会被添加到全局对象中。变量声明后
只能在当前文件中使用。

模块成员导出与导入

在Node.js环境中,默认就支持模块系统,该模块系统准寻CommonJS规范。
一个JavaScript文件就是一个模块,在模块文件中定义的变量和函数默认
只能在模块文件内部使用。如果需要在其他文件中使用。
必须显式声明将其进行导出。

模块成员导出
在每一个模块文件中,都会存在一个module对象,即为模块对象。在模块对象中保存了和当前模块相关信息。
在模块对象中有一个属性exports,它的值是一个对象。模块内部需要被导出的成员都应该存储道这个对象中。

模块成员导入
在其他文件中通过require方法引入模块,require方法的返回值就是对象模块的module.exports对象。
在导入模块时,模块文件后缀.js可以省略,文件路劲不可省略。
require方法属于同步导入模块,模块导入后可以立即使用.
通过reuqire方法导入模块,模块当中的文件代码会被立即执行。
const logger = require("./logger");
const _ = require("lodash");
(比如console.log())

在导入其他模块时,建议使用const关键字声明常量。防止模块被重置

如果只有一个方法的情况下在导出时可以直接将module.exports = ()=>{}
重置为一个对象.导入被导出的文件,可以直接使用被导出的模块名,直接调用.

模块包装函数

Module Wrapper Function

Node.js是如何实现模块的,为什么在模块文件内部定义的变量.
在模块文件外部访问不到?
每一个模块中都有module和require方法,它们是从哪里来的?

在模块文件执行之前,模块文件中的代码会被包裹在模块包装函数中。
这样每个模块文件中的代码都拥有了自己的作用域。所以在模块外部
就不能访问模块内部的成员了。

(function(exports,require,module,__filename,__dirname){
    code
})

__filename 文件路径
__dirname 文件目录

在这个模块包装函数中可以看到,module和require实际上是模块内部成员。不是全局对象
global下面的成员。
exports: 引用地址指向了module,exports对象,可以理解为是module.exports对象
的简写形式.
在导入模块时最终导入的是module.exports对象,所以在使用exports对象添加导出
成员时不能修改引用地址。也就是说不能够通过exports重置module.exports

内置模块

node.js会内置一些非常有用的模块。
Path: 模块内提供了一些和路径操作相关的方法。
File system (fs): 文件操作系统,提供了和操作文件相关的方法。
在引入内置模块时,使用的是模块的名字.前面不需要加任何路径。

path:
const file = path.parse("./");能够解析一个文件路径,以对象形式返回。

fs:
fs.readdirSync(); // 同步方法,得到响应目录所有的文件
fs.readdir("./",function(error,files){

}); // 异步方法,得到响应目录所有的文件
异步方法大多都是通过回调函数来拿结果的。

NPM概述

每一个基于Node.js平台开发的应用程序都是Node.js软件包。
所有Node.js软件包都被托管在www.npmjs.com中。

什么是NPM
Node package Manager,Node.js环境中的软件包管理器。
随Node.js一起被安装。

它可以将Node软件包添加到我们的应用程序中,并对其进行管理。
比如下载,删除.更新,查看版本等等。

package.json

package.json
它是应用程序的描述文件,包含应用程序相关信息,比如应用名称.
应用版本,应用作者等等。

通过package.json文件可以方便管理应用和发布应用
创建package.json文件: npm init
快速创建package.json文件: npm init --yes

package name: 包名
version: 版本号
description: 软件描述
entry point: 应用入口文件
test command: 测试命令
git repository: git地址
keywoids: 应用关键字
author: 应用作者
license: ISC 应用协议
dependencies: 软件依赖包

使用NPM命令下载软件包

在应用程序的根目录执行命令。
npm install <pkg>或者npm i <pkg>

软件包下载完成后会发生三件事:
软件包会被存储在node_modules文件夹中,如果在应用中不存在此文件夹。
npm会自动创建。
软件包会被记录在package.json文件中.包含软件包的名字以及版本号。
npm会在应用中创建package-lock.json文件。用于记录软件包及
软件包的依赖包的下载地址及版本。

使用Node.js软件包

在引入第三方软件包时,在require方法中不需要加入路径信息。只需要使用软件包
的名字即可。require方法会自动去node_modules文件夹中进行查找。

git与node平台的包

可以通过.gitignore文件对不需要的文件夹进行配置.
node_modules/

git init 创建git仓库
git status 查看git add文件,包含的提交内容
git add. 将仓库文件提交到暂存区当中.
git commit -m 将文件提交到 git仓库中
git remote add origin 地址 将git仓库文件提交到远端仓库

git下载源码,回复包

没有包,但是package.json文件中会包含软件包的名字。
可以在目录通过npm install进行安装.npm会自动解析package.json
中的dependencies,查找依赖包.进行下载。

语义版本控制说明

Maior Version主要版本: x.00.00
添加新功能破坏现有API

Minor version 次要版本: 1.XX.00
不会破坏现有API,在现有API的基础上进行添加.

patch version 补丁版本: 3.56.XX
用于修复bug

版本号更新规范:
^5.12.5: 主要版本不变,更新次要版本和补丁版本.
~5.12.5: 主要版本和次要版本不变,更新补丁版本.
5.12.5: 使用确切版本,即主要版本,补丁版本固定

查看软件包实例版本的两个方法

通过 npm list

如何查看软件包元数据

通过 npm view name  查看软件包元数据
npm view name versions 查看软件包元数据版本
npm view name dist-tags dependencies 查看软件包元数据多项。

下载特定版本的软件包和删除软件包

npm install jq@版本号
删除软件包:
npm uninstall jq

更新软件包

通过npm outdated命令可以查看哪些软件包已经过期.对应的新版本是什么。
通过npm update更新过期的软件包,更新操作遵循语义版本控制规则。

项目依赖和开发依赖

项目依赖:
无论在应用开发环境还是线上环境,只要程序在运行的过程中,需要使用的软件包
就是项目依赖。比如lodash,mongoose。

开发依赖: 在应用开发阶段使用,在生产环境中不需要使用的软件包。
比如TypeScript中的类型声明文件。比如less预编译

在下载开发依赖时,项目依赖和开发依赖要分别记录。
项目依赖被记录在dependencies对象中,开发依赖被记录在devDependencies中,
使开发者可以在不同的环境中下载不同的依赖软件包。

在下载开发依赖时,要在命令后面加上 --save-dev或者-D选项。
比如 npm install less -D

在开发环境中下载所有的软件依赖包: npm install
在生产环境中只下载项目依赖软件包: npm install --prod

本地安装和全局安装

本地安装: 将软件包下载到应用根目录下的node_modules文件夹中,软件包只能
在当前应用中使用。
全局安装: 将软件包下载到操作系统的指定目录中,可以在任何应用中使用。
一般来说项目依赖都是本地安装的。开发依赖是安装到全局的。

通过-g选项将软件包安装到全局: npm install name -g
查看全局软件包的安装位置: npm root -g
删除全局中的软件包: npm uninstall -g
查看全局安装了哪些软件包: npm list -g --depth 0

nodemon模块:
    能够在编写node代码时,即时预览。它没有提供实质性功能和API属于开发依赖。
    通过nodemon执行nodejs代码和模块.

强制更新软件包版本-模块

npm-check-updates强制更新

npm-check-updates可以查看有哪些软件包过期了。可以强制更新package.json
文件中软件包版本。
1.将npm-check-updates安装到全局: npm install npm-check-updates -g
2.查看过期的软件包: npm-check-updates
3.更新package.json: ncu -u
4.安装软件包: npm install

发布软件包

1.注册npm账号。
2.创建软件包
    npm init --yes
3.创建index.js模块
4.登录npm{npm镜像地址必须氛围npmjs.com}
    npm login
5.发布软件包
    npm publish

更新软件包版本号

在软件包的源代码发生更改后,是不能直接发布的。应该更新软件包的版本号,然后再进行发布。
更新主要版本号: npm version major
更新次要版本号: npm version minor
更新补丁版本号: npm version ptach

撤销已经发布的软件包

只有在发布软件包的24小时之内才允许被撤销。
软件包被撤销24小时候,才能够重新发布。
重新发布需要修改名称和版本号

npm unpublish name --force 撤销软件包发布

更改NPM镜像地址

由于npmjs.com是国外的网站,大多数时候下载软件包的速度会比较慢。
可以通过配置的方式更改npm工具的下载地址.

获取npm配置:
    npm config list -l --json
    -l列表所有默认配置选项。
    --json 以json格式显示配置选项。

设置npm配置
    获取npm下载地址: npm config get registry
    获取npm用户配置文件: npm config get userconfig

更改npm镜像地址
    npm config set registry https://registry.npm.taobao.org
    npm config set registry https://registry.npmjs.org/

    .npmrc

npx命令的两个用途

npx是npm软件包提供的命令。它是node.js平台下软件包执行器。
主要用途有两个:
第一个是临时安装软件包执行后删除它。
第二个是执行本地安装的提供命令的软件包。

1.临时安装软件包后删除软件包
有些提供命令的软件包使用的频率并不高。比如create-react-app
脚手架工具。我能不能临时下载使用再删掉它。
npx create-react-app react-test

2.执行本地安装的软件包
现在有两个项目都依赖了某个命令工具软件包,但是项目A依赖的是它的1版本,
项目B依赖的是它的2版本,我在全局到底该安装什么版本。
该软件包可以在本地进行安装,在A项目中安装它的1版本,在B项目中安装它的2版本,
在应用中可以通过npx调用node_modules文件夹中安装的命令工具。
比如:
    在本地安装了命令工具,无法通过命令直接执行。
    本地nodemon无法直接执行。可以使用npm nodemon xxx.js进行执行。

将所有软件包安装到应用本地是现在最推荐的做法,一是可以防止软件包的版本冲突问题。
二是,其他开发者在恢复应用依赖时可以恢复全部依赖。
因为软件包安装到本地后会被package.json文件记录。其他开发者在运行项目时,
不会因为缺少依赖而报错。

配置入口文件的作用

应用程序的入口文件就是应用程序执行的起点,就是启动应用程序时执行的文件。
场景一: 其他开发者拿到你的软件包以后,通过该文件可以知道入口文件是谁。
通过入口文件启动应用。

场景二: 通过node应用文件夹命令启动应用,node命令会执行package。json文件中
main选项指定的入口文件。如果没有指定入口文件,则执行index.js

模块查找规则

1.在指定了查找路径的情况下
require("../server");
1.查找server.js
2.查找server.json
3.查找server文件夹,查看入口文件(package.json->main)
4.查找server文件夹中的index.js文件

在没有指定查找路径的情况下:
    require("nice");
    1.查找是不是系统模块
    然后就按照module对象内的paths规则进行查找。
    paths规则查询的步骤又同时遵循上面的规则
    paths: [
        'D:\\大前端的学习\\案例\\Part3\\model1\\node_modules',
        'D:\\大前端的学习\\案例\\Part3\\node_modules',
        'D:\\大前端的学习\\案例\\node_modules',
        'D:\\大前端的学习\\node_modules',
        'D:\\node_modules'
    ]

CPU与存储器

目标: 了解程序运行过程中CPU和存储器起到了什么作用或者扮演了什么角色。
1.CPU
    中央处理器,计算机核心部件,负责运算和指令调用。
    开发者编写的JavaScript代码在被编译为机器码以后就是通过CPU执行的。
2.存储器

输入输出操作及模型介绍

I/O操作模型有2种。
第一种是同步I/O操作,也叫做阻塞IO操作
第二种是异步I/O操作,也叫做非阻塞操作。

Node.js采用的业务模型是,异步非阻塞IO操作。

进程与线程

进程就是程序运行时的实例对象。
线程是包含在应用程序中,代办的事情。

JavaScript是单线程还是多线程的

在Node.js代码运行环境中,它为JavaScript代码的执行
提供了一个主线程。我们常说的单线程就指的就是这个主线程。
主线程用来执行所有的同步代码。但是Node.js代码运行环境
本身是由C++开发的。在Node.js内部它依赖了一个叫做libuv的
c++库。在这个库中它维护了一个线城池。默认情况下这个线城池存储了4
个线程。
JavaScript中的异步代码就是在这些线程中执行的。所以说JavaScript代码
运行依靠了不止一个线程。所以JavaScript本质上还是多线程的。

什么是回调函数

回调函数是指通过函数参数的方式将一个函数传递到另一个函数中。
参数函数就是回调函数。

回调函数在异步编程中的应用

const fs = require("fs");
let txt;
fs.readFile("./1.txt","utf-8",function(derr,ata){
    txt = ata;
    console.log(txt);
});

回调地狱问题

回调地狱是回调函数多层嵌套导致代码难以维护的问题。
基于回调函数的异步编程,一不小心就会产生回调地狱的问题。

promise基本用法

promise是JavaScript中异步编程解决方法,可以解决回调函数方案中的回调地狱问题。
可以将Promise理解为容器,用于包裹异步API容易。
当容器中的API执行完成后,Promise允许我们在容器外面获取异步API的执行结果。
从而避免回调函数嵌套。

Promise翻译为承诺,表示它承诺帮我们做一些事情,既然它承诺了。它就要去做。
做就会有一个过程,就会有一个结果,结果要么成功要么是失败。
所以在Promise中有三种状态,分别为等待(pending),成功(fulfilled),失败(rejected)。
案例:
const promise = new Promise(function(resolve,reject){
    // resolve方法的作用将等待状态变成成功状态
    // reject方法的作用将等待状态变成失败状态
    fs.readFile("./x.txt","utf-8",function(error,data){
        if(error) {
            // 如果error为真不为null,则为错误.
            // 则执行reject。把promise返回结果设置为错误。
            reject(error);
        } else {
            // 结果不为错误那么肯定是成功。
            // 调用成功方法resolve,从等待变为成功。
            resolve(data);
        }
    })
});
promise.then(function(data){
    // 通过promise对象的.then方法可以接收成功的数据.
    console.log(data);
}).catch(funtion(error){
    // 通过promise对象的.catch方法可以接收失败的数据。
    console.log(error);
})

通过Promise解决回调地狱

// Promise回调地狱解决方案
const fs = require("fs");
function readFile(path){
    return  new Promise(function(resolve,reject){
        fs.readFile(path,"utf-8",function(error,resule){
            if(error){
                reject(error);
            } else{
                resolve(resule);
            }
        });
    });
}
readFile("./1.txt")
.then(function(data){
    console.log(data);
    return readFile("./2.txt");
})
.then(function(data){
    console.log(data);
}).catch(function(error){
    console.log(error);
    // 错误时调用,打印错误信息
}).finally(function(){
    // 无论错误或成功都会调用
    // finally回调函数是没有参数的
    console.log("finally");
});

使用Promise.all方法执行并发操作

Promise.all([])方法内放置的就是Promise数组对象;

const fs = require("fs");
function readFile(path){
    return  new Promise(function(resolve,reject){
        fs.readFile(path,"utf-8",function(error,resule){
            if(error){
                reject(error);
            } else{
                resolve(resule);
            }
        });
    });
}
Promise.all([
    readFile("./1.txt"),
    readFile("./2.txt")])
.then(function(result){
    console.log(result);
});

使用异步函数解决Promise代码臃肿的问题

const fs = require("fs");
function readFile(path){
    return  new Promise(function(resolve,reject){
        fs.readFile(path,"utf-8",function(error,resule){
            if(error){
                reject(error);
            } else{
                resolve(resule);
            }
        });
    });
}
async function run(){
    let x = await readFile("./1.txt");
    let y = await readFile("./2.txt");
    return [x,y];
}
run().then(data=>console.log(data));

通过promisify方法改造通过回调函数获取结果的异步API

Promisify函数-异步函数promise化

const fs = require("fs");
const promisify = require("util").promisify;
const readFile = promisify(fs.readFile);
async function run(){
    let x = await readFile("./1.txt","utf-8");
    let y = await readFile("./2.txt","utf-8");
    return [x,y];
}
run().then(data=>console.log(data));

Event Loop事件循环机制

Event Loop事件概述

为什么要学习事件循环机制
学习事件循环机制可以让开发者明白JavaScript的运行机制是怎么样的。

为什么叫做事件循环机制?
因为Node.js是事件驱动的。事件驱动就是当什么时候做什么事情,做的事情就是
定义在回调函数中的。可以将API的回调函数理解为事件处理函数。
所以管理异步API回调函数什么时候回到主线程中调用的机制叫做事件循环机制。

事件循环的六个阶段

1.Timers: 用于存储定时器的回调函数(setinterval,setTimeout);
2.Pending callbacks:执行与操作系统相关的回调函数,比如启动服务器端应用时
监听端口操作的回调函数就在这里调用。
3.ldle,prepare: 系统内部使用
4.IO Poll: 存储I/O操作的回调函数队列。比如文件读写操作的回调函数。
5.Check: 存储了setlmmediate API的回调函数。
6.Closing callbacks: 执行与关闭事件相关的回调,例如关闭数据库链接的回调函数等。
循环体会不断运行以检测是否存在没有调用的回调函数,事件循环机制会按照先进先出的方式
执行。他们直到队列为空。

宏任务与微任务

宏任务:setInterval、setTimeout、setimmediate、I/O
微任务: Promise.then、Promise.catch、Promise.finally、process.nextTick

1.微任务的回调函数被放置在微任务队列中,宏任务的回调函数被放置在宏任务的队列中。
2.微任务优先级高于宏任务
    在微任务中,nextTick的优先级要高于microTask.
    只有nextTick中的所有回调函数执行完成后才会开始执行microTask。

    在宏任务中是没有优先级的概念的,他们的执行顺序是按照事件循环阶段进行的。
    setTimeout延迟为0,js执行器也会默认+1.所以SetTimeout优先级比Setmmediate优先级高。

通过代码验证事件循环机制

在Node应用程序启动后,并不会立即进入时间循环,而是先执行输入代码。从上到下开始执行。
同步API立即执行,异步API交给C++维护的线程执行,异步API的回调函数,被注册到对应的
事件队列中,当所有代码执行完成后,开始进入事件循环。

1:
console.log("start");
setTimeout(()=>{
    console.log("timeou_1");
},0);
setTimeout(()=>{
    console.log("timeou_2");
},0);
console.log("end");
// 运行结果为:
// start end 1 2

nextTick方法

process.nextTick()方法.
process.nextTick(callback,[...args])
callback 回调函数
args 调用callback时额外传的参数

此方法的回调函数优先级最高,会在事件循环之前被调用。
如果你希望异步任务尽早地执行,那就使用process.nextTick。

const fs = require("fs");
function readFile(fileName,callback){
    if(typeof fileName !== "string"){
        return callback(new TypeError("filename,必须是字符串类型"));
    }
    fs.readFile(filename,function(error,datas){
        if(error) {
            return callback(error);
        }
        return callback(data);
    });
}
// 上述代码的问题在于readFile方法根据传入的参数类型,callback可能会在主线程中
直接被调用。callback也可能在事件循环的IO轮询阶段直接被调用。
这可能会导致不可预测的问题发生。如何使用readFile方法变成完全异步呢?
const fs = require("fs");
function readFile(fileName,callback){
    if(typeof fileName !== "string"){
        return process.nextTick(callback,new TypeError("filename,必须是字符串类型"));
    }
    fs.readFile(filename,function(error,datas){
        if(error) {
            return callback(error);
        }
        return callback(data);
    });
}

setlmmediate方法

setlmmediate(ballback,[...args]);
setimmediate表示立即执行,它是宏任务,回调函数会被放置在事件循环的check阶段。
在应用中如果有大量的计算型任务,它是不适合放在主线程中执行的。因为计算任务
会阻塞主线程,主线程一旦被阻塞,其他任务就需要等待。所以这种类型的任务,
最好交给c++维护的线程去执行。

可以通过setlmmediate方法将任务放入事件循环中的check阶段,因为代码在这个阶段
执行不会阻塞主线程,也不会阻塞事件循环。

setlmmediate();

结论

Node适合I/O密集型任务,不适合CPU密集型任务,因为主线程一旦阻塞,程序就卡了。

网站的概述

网站的组成

从开发者角度来看,web应用主要由三部分组成:用户界面、业务逻辑、数据
1.用户界面(视图层): 用于将数据展示给用户的地方,采用HTML、CSS、JavaScript编写。
2.业务逻辑(控制层): 实现业务逻辑和控制业务流程的地方,可以采用Java、PHP、pyhton、js编写。
3.数据(模型层): 应用的核心部分,应用业务逻辑的实现,用户界面的战士都是基于数据的。web应用中的数据通常是存储在数据库中的。数据库可以采用Mysql、Mongodb等。

什么是web服务器

服务器是指能够向外部(局域网或万维网)提供服务的机器(计算机)就是服务器
在硬件层面,web服务器就是能够向外部提供网站访问服务的计算机。
在这台计算机中存储了网站运行所必须的代码文件和资源文件。
在软件层面,web服务器控制着用户如何访问网站中的资源文件,控制着用户如何与网站进行交互。

客户端

web应用中的客户端是指用户界面的载体,实际上就是浏览器。
用户可以通过浏览器这个客户端访问网站应用的界面,通过用户界面与网站应用进行交互。

网站的运行

web应用是基于请求和响应模型的。

IP和域名

IP:互联网协议地址,标识网络中设备的地址,具有唯一性。

域名: 是由一串用点分隔的字符组成的互联网上某一台计算机或计算机组的名称。
用于在传输时标识计算机的电子方位。

DNS服务器

域名服务器,互联网域名解析系统。它可以将人类可识别的标识符映射为系统内部
通常为数字形式的标识码。

端口

是设备与外界通讯交流的出口。0-65535
通常web应用占用端口80,在浏览器中访问应用时80可以省略。
因为默认就访问80.

URL

URL:统一资源定位符,表示我们要访问的资源在哪,以及要访问的资源是什么。

前台和后台,前端与后端

前台和后台都是指用户界面,前台是为用户准备的,每个人都可以访问的用户界面。
后台是为网站管理员准备的,只有登录以后才能访问的用户界面,用于管理网站应用
中的数据。

前端: 指开发客户端应用的程序员
后端: 指开发服务器端应用程序的程序员

开发环境说明

在开发环境中,开发机器既充当了客户端又充当了服务端。
本机IP: 127.0.0.1
本机域名: localhost

创建webserver

创建软件层面的web服务器,用于资源要如何被访问
在node.js中,环境提供了一个系统模块叫做http.
可以通过这个模块,创建http协议
const http = require("http");
const server = http.createServer((req,res)=>{
    // req请求对象,包含请求信息
    // res响应对象,用于对请求进行响应
    if(req.url === "/"){
        res.write("hellow,node");
        res.end();
    }

});
server.listen(3000); // 监听端口
console.log("服务器启动成功");

脚手架工具

脚手架的作用

创建项目基础结构,提供项目规范和约定

脚手架的分类

通用型脚手架 yeoman
专用型脚手架 create-react-app vue-cli angular-cli

Yeoman

yeoman的基本概念

yeoman是一款脚手架工具
可以帮助开发人员创建项目的基础结构代码

yo是Yeoman的命令行管理工具
可以在命令行运行yeoman的命令

生成器:Yeoman中具体的脚手架
针对不同项目有不同的脚手架

Yeoman使用说明

全局安装yo npm install -g yo
安装generator npm install -g generator-webapp
通过yo运行generator yo webapp
启动应用 npm run start

自动化构建

什么是构建

将源代码转换成生产代码

为什么构建

一些代码需要编译
将Less或Sass转换成CSS
将ES6+的新语法转换成ES5

有一些的代码需要压缩(CSS、JS、HTML)
压缩之后的代码体积更小,加载更快,节省带宽

有些代码需要格式化效验,统一代码风格

less转css

安装less插件
通过lessc命令转换

什么是自动化构建

自动化构建是指将手动构建任务,通过命令自动执行的过程

npmscript

npm允许在package.json文件中,使用script字段定义脚本命令

NPM SCRIPT自定义脚本命令:
1.声明命令:
"script":{
    "foo": "node bar.js"
}
2.执行命令:
    npm run foo

npm script命令的执行方式

并行执行: 任务之间没有先后顺序,同时执行可以提高执行效率。 任务1 & 任务2
串行执行: 任务之间有先后顺序,先执行前一个任务,后执行下一个。 任务1 && 任务2

并行执行在windows下是不起作用的。
npm-run-all插件可以解决上面的问题.

npm-run-all -p 并行执行
run-p

npm-run-all -s 串行执行
run-s

minify压缩构建工具

将less转成css
    npm i less -g
    lessc input.less output.css

压缩css文件
    npm i minify -g
    minify output.css>output.min.css

Babel-js构建工具

Babel插件可以将ES6+转换成ES5语法

babel-preset-env

babel转换命令:
babel input.js -o es5input.js 转换单个文件
bable js -d es5js 转换整个目录

安装方法:
    安装Babel: npm install -g babel-core babel-cli
    安装转码规则: npm install -D babel-preset-env 开发依赖,安装非全局
    配置转换规则: 创建文件 .babelrc
    {
        "presets": [
            "env"
        ]
    }
    在npm script中添加转换命令: babel src -d dist

代码格式校验

不同的工程师,写的代码风格不同
项目代码提交时,需要保持统一的代码格式

如何实现(通过工具完成代码格式效验)
提供编码规范
根据编码规范,自动检查代码

ESLint-JS代码格式检查

ESlint对JavaScript代码格式进行检查

使用ESLint
初始化项目npm init --yes
安装ESLint (npm install eslint -g)
属于开发依赖,建议安装在全局.

初始化配置文件: eslint --init

检查JS代码格式:
    单个文件: eslint ./js/index.js
    整个目录: eslint ./js

eslint的配置
"rules": {
    "indent": ["off",2], //设置缩进
    "quotes": ["error","double"] //设置字符串格式为双引号
    // off 关闭
    // warn 有警告不报错
    // error 直接报错
}

StyleLint-CSS代码格式校验

使用StyleLint
初始化项目: npm init --yes
安装StyleLint npm install -g stylelint
安装检测标准 npm install --global stylelint-config-standard
创建配置文件: .stylelintrc.json
{
    "extends": "stylelint-config-standard", // 设置两个代码为缩进
    "rules": {
        "block-no-empty": true // 设置代码不能为空
    }
}

检测CSS代码格式:
    单个文件: stylelint ./filename.css
    整个css文件夹: stylelint ./css/*.css
    整个项目:  stylelint ./**/*.css

CSS代码格式标准符号
/* body的注释 */
body {
  margin: 0 auto;
  color: red;
}

Gulp自动化构建工具

gulp概述

Gulp与npm script都能够实现自动化构建

Gulp语法简单:
    Gulp语法就是JavaScript语法
    npm script 语法接近shell脚本
    Gulp生态完善,构建效率高

gulp基本使用

全局安装gulp客户端(npm install gulp-cli -g)
初始化项目: npm init --y
安装gulp包: npm install gulp -D
新建gulpfile文件: gulpfile.js
在gulpfile.js中,创建gulp任务
执行gulp任务 gulp name

gulp配置

const task1 = (cb)=>{
    console.log('Task 1 is running');
    cb();
};
module.exports = {
    task1,
    default: task2 // 默认任务
};

gulp组合任务

gulp并行执行任务:
    gulp.parallel(task1,task2);
gulp串行执行任务:
    gulp.series(task1,task2,task3);

Gulp文件操作

文件操作-缓冲方式:
源文件->内存缓冲->目标文件

文件操作-流方式:
把文件分成碎片,一点一点操作

Gulp是基于流的构建系统:
    输入->加工->输出
    读取流->转换流->写入流
    src()->pipe()->dest()
案例:
    // 引入gulp
    const gulp = require("gulp");
    // 声明gulp任务
    const style = ()=>{
        // 流 就是异步操作
        return gulp.src('src/styles/main.css',{base: 'src'}).pipe(gulp.dest('dist'));
        // src内的第二个参数: base: 'src'表示参考目录
    };
    exports.style = style;

    // 通过解构方式引入gulp内的函数
    const {src,dest} = require("gulp");

    // 关于文件匹配:
    gulp.src("src/images/**"); // 表示匹配目录下的所有文件
    gulp.src("src/css"/*.js);表示匹目录下的js文件

构建样式文件

gulp-less,less转css的插件
gulp-babel 将ES6+语法转换成ES5
gulp-uglify 压缩js插件
gulp-clean-css 压缩css插件
gulp-rename 重命名文件

案例:
// 引入gulp
const gulp = require("gulp");
const less = require("gulp-less");
const cleanCss = require("gulp-clean-css");
const rename = require("gulp-rename");
// 声明gulp任务
const style = ()=>{
    // 流 就是异步操作
    return gulp.src('src/styles/main.css',{base: 'src'}) // 引入css文件
    .pipe(less()) // less转css
    .pipe(cleanCss()) // css压缩代码
    .pipe(rename({"extname": ".min.css"})) // 文件后缀重命名
    .pipe(gulp.dest('dist')); // 输出文件的目录
};
exports.style = style;

CSS hack与Autoprefixer

CSS hack概述

CSS hack - CSS代码存在兼容性问题
同一段代码,在不同浏览器上呈现的效果不同

针对不同浏览器写相应CSS代码
针对不同浏览器写相应CSS代码的过程,叫做CSS hack
CSS hack目的: 就是使CSS代码兼容不同的浏览器

CSS hack-属性前缀法

user-select属性可以控制用户能否选中文本(存在兼容性问题)
-webkit-user-select: none;
给CSS属性,添加特有的前缀

浏览器的CSS属性前缀

IE: -ms-
Chrome: -webkit-
Safari: -webkit-
Firefox: -moz-
Opera: -o-

Autoprefixer插件

Autoprefixer使用caniuse.com的数据来决定
哪些属性需要加前缀。
安装代码:
npm install gulp-autoprefixer -D

return gulp.src('src/styles/main.less',{base: 'src'})
.pipe(less())
.pipe(autoprefixer()) // autoprefixer
.pipe(cleanCss())
.pipe(rename({"extname": ".min.css"}))
.pipe(gulp.dest('dist'));

js构建-gulp

.pipe(babel({
    presets: ["babel-preset-env"]
}))
// ES6转ES5插件babel
gulp-babel使用前需要前置开发插件,babel

安装配置参考官网
安装命令(参考前置依赖babel版本):
# Babel 7
$ npm install --save-dev gulp-babel @babel/core @babel/preset-env

# Babel 6
$ npm install --save-dev gulp-babel@7 babel-core babel-preset-env

案例:
const script = ()=>{
    return gulp.src('src/js/main.js',{base: 'src'})
    .pipe(babel({
        presets: ["babel-preset-env"]
    }))
    .pipe(uglify())
    .pipe(rename({"extname": ".min.js"}))
    .pipe(gulp.dest("dist"));
};

HTML构建

<!-- HTML的压缩 -->
使用插件gulp-htmlmin --压缩HTML文件
安装插件:
    npm install gulp-htmlmin -D

gulpfile内使用参考官网

案例:
    return gulp.src('src/index.html',{base: "src"})
    .pipe(htmlmin({
        "collapseWhitespace": true, // 折叠HTML内空白字符
        "minifyCSS": true, // 折叠HTML嵌入CSS样式
        "minifyJS": true // 折叠Script标签内的嵌入JS
    }))
    .pipe(gulp.dest('dist'));

构建图片文件

Glup图片文件所需插件
gulp-imagemin 压缩图片文件

npm install gulp-imagemin -D

文件清除

Gulp清除文件所需插件
del 删除文件和目录

安装插件:
npm install del -D

发布服务

brower-sync 发布web服务
npm install --save-dev browser-sync

brower-sync.init({
    notify: false, // 禁用浏览器右上角的browerSync connected
    server: {
        baseDir: './dist',
        routes: {
            '/Gulp/05—gulp/03/node_modules': 'node_modules'
        }
    }
})

gulp中使用bootstrap

先在项目中安装bootstrap
npm install bootstrap@3.4.1 jquery -S

sr.init({
    notify: false, // 禁用浏览器右上角的browerSync connected
    server: {
        baseDir: 'dist',
        routes: {
            '/node_modules': 'node_modules'
        }
    }
});

<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">

Reload监视文件变化

gulp.watch('src/index.html',html);
// 被监视的文件对应任务
sr.init({
    notify: false, // 禁用浏览器右上角的browerSync connected
    files: 'dist/**',// 监视DIST下的文件变化然后在浏览器上更新
    server: {
        baseDir: 'dist',
        routes: {
            '/node_modules': 'node_modules'
        }
    }
});

Gulp与Yeoman

server自动化构建任务
npm run start->package.json->script->star->gulp server->gulpfile.js

Gulp + npmscript = 脚手架命令

模块化开发

模块化概述

模块化只是思想

模块化的演进过程

1:早起模块化完全依靠约定

2:后面依靠命名空间方式.
每个模块放入不同的对象内

3:IIFE立即执行函数,暴露给全局对象
确保了私有成员安全

上面是在早期没有工具和规范的情况下对模块化的落地方式.

模块化规范的出现

模块化标准+模块加载器

CommonJS规范:
    每一个文件就是一个文件
    每个模块都有单独的作用域
    通过module.exports导出成员
    通过require函数载入模块
    (CommonJS是以同步模式加载模块)

早期并没有选择CommonJS规范
而是选择了:
AMD规范、采用了Require.js
define();定义模块
reuire()加载模块
目前绝大多数库,都支持AMD规范
AMD使用起来相对复杂.模块JS文件请求频繁

CMD规范,类似commonjs

模块化标准规范

在NodeJS平台遵循CommonJS规范.
在浏览器中使用ES Modules规范

ES Modules是于ECMAScript 2015被定义的标准。
存在各种各样的兼容问题。
随着webpack等打包工具的流行,ESModules才变为主流。

ES Modules特性

通过给script标签添加type = module的属性,
就可以以ES Module的标准执行其中的js代码了

ES Modules自动采用严格模式,忽略 'use strict'
每个ES Modules都是有独立的作用域

ES Modules是通过CORS进行请求外部JS模块的,后端不支持跨域会失败

ES Modules的SCRIPT标签会延迟执行脚本,等待HTML加载完毕

ESModules导出

导出:
    export let name = 'foo module';
    export {name,age,sex};
导入:
    import name from './module.js';
    import {name,age,sex} from './module.js';
    console.log(name);

导出重命名:
    export {name as foname,age as foage,sex as fosex};
导入重命名:
    import {foname as name,foage as name,fosex as name} from './module.js';

ESModules导入导出的注意事项

export 导出的成员并不是对象字面量,类似于解构,是固定用法
如果想导出一个字面量,在导出成员前添加 default关键字
export default {name,age};

import 导入的成员也并不是对象字面量.类似于解构,是固定用法
是传址是引用关系,不是传值。
import导入成员是只读的,在没办法去修改引入模块内的成员。
只能在被引入的外部模块内修改(比如外部模块所提供的方法)

ESModules 导入用法

不同于CommonJS中的require的导入,ESModules导入成员.
是需要带完整路径与后缀名的。
import {luccy} from "./index,js";
相对文件路径不能省略 ./

import只导入不提取成员:
    import "./module.js";

批量导入模块成员:
    import * as mod form './module.js';
    // 一次性提取所有导出的成员,放入mod对象

import关键字只能在最外层作用域.

import方法 动态导入模块:
    import("./module.js").then(funciton(module){
        console.log(module);
        // 回调导入的module对象,为导入模块所导出的成员。
    });

import 方法导入默认成员:
    简写: 默认成员,{导入成员}
    import abc, {foname as name} from "./module.js";

    全写:
    import {foname as name,default as tittle} from "./module.js";

ESModules导出导入成员

export 可以把导入成员,当做导出成员导出
这种做法通常在index.js内使用。
index.js:
    import {button} form "./button.js";
    import {avatar} form "./avatar.js";
    export {button,avatar};

app.js:
    import {button,avatar} form "./components/index.js";

这样就可以在app.js内一次性导入多个模块

ESModules浏览器环境兼容Polyfill

在一些低版本浏览器中,可以使用browser-es-module-loader.js插件来兼容js.
npm install browser-es-module-loader

但是,使用插件后会在支持ESModules的浏览器会工作两次。
如果想解决这个问题可以使用es6新增的属性:nomodule,
在script标签内。
有了 nomodule 属性,在支持ESModules的浏览器中,
便不会加载这个代码了。

ESModules in Node.js支持情况

node.js逐渐开始支持ESModules。
但是,目前的特性还是处于过渡状态.

Node.js内使用ESM后缀名需要为.mjs

可以像commonJS一样使用解构把模块内的方法拿出来
import fs from "fs";
const {writeFileSync} = fs;
writeFileSync("./foo.txt",'es module working');

node系统内置模块有兼容语法:
    import {writeFileSync} from "fs";
    // 可以选择性导入fs模块内响应的对象.

ESM与CommonJS模块交互

import foo from "./common.js";
console.log(foo);

ESM语法可以引入CommonJS模块.
CommonJS模块始终只会导出一个默认成员.
只能用默认加载语法来引入CommonJS模块.
不能使用{}语法:
    import {foo as name} "./common.js";
    // 以上写法是错误的.

导入后可以使用解构方法把模块拿出来:
import foo from "./common.js";
const {foo: name} = foo;
console.log(name);

CommonJS语法不能引入ESM模块

ESM和CommonJS的差异

ESM没有CommonJS中的那些模块全局成员了。
比如: __filename 、 __dirname

ESM中可以通过以下方式拿到以上两个全局变量
import {fileURLToPath} from 'url';
const __filename = fileURLToPath(import.meta.url);

import {dirname} from 'path';
const __dirname = dirname(__filename);

package配置使用ESM

在package.json中添加type成员,设置为默认使用ESM。
{
    "type": "module"
}

在package.json中设置为默认EMS后,如果想用CommonJS
将js后缀改为: .cjs便可以使用CommonJS了。

早期node.js兼容

babel-node让早期Node.js平台使用新语法和特性.

babel通过插件转换特性:
arrow-function->箭头函数
classes->类
destructuring->解构
module-commonjs->ES Modules
etc.->其他特性

webpack打包

webpack概述

webpack = Web Package
Webpack是一个现代JS应用程序的静态模块打包器(module bundler)
    模块(模块化开发,可以提高开发效率,避免重复造轮子)
    打包(将各个模块,按照一定规则组装起来)
    特点
        功能强大(打包、构建、发布Web服务)
        学习成本高

打包过程:就是把多个文件合并成一个文件

源代码:
{
    index.js,nav.js,tab.js
}->
打包工具:
{
    编译->格式化校验->压缩=>打包
}->
生产代码:
bundle.js

Webpack功能

Webpack = 编译=>格式化校验=>压缩=>打包
将多个文件合并(打包),减少HTTP请求次数,从而提高效率
对代码进行编译,确保浏览器兼容性.(ES6+ —— ES5)
检测代码格式,确保代码质量
提供热更新(LiveServer),提高开发效率
针对不同环境,提供不同的打包策略。

Webpack核心概念

入口、出口、加载器、插件、模式
模块、依赖图

入口

入口: 打包时,第一个被访问的源码文件
默认是: src/index.js(可以通过配置文件指定)
webpack可以通过入口,加载整个项目的依赖

出口

出口: 打包后输出文件的名称\
默认是: dist/main.js (可以通过配置文件指定)

loader-加载器

loader
专门用来处理一类文件(非JS)的工具
Webpack默认只能识别JS,想要处理其他类型的文件,
需要对应的Loader

命名方式:
xxx-loader
css-loader 、 html-loader、file-loader
以-loader为后缀

常用加载器:
https://www.webpackjs.com/loaders

plugin-插件

实现loader之外的其他功能

plugin是webpack的支柱,用来实现丰富的功能。
命名方式:
xxx-webpack-plugin (html-webpack-plugin)
以-webpack-plugin为后缀

常用插件:
https://www.webpackjs.com/plugins
loader和plugin总结
Loader和Plugin本质上都是npm包

模式

模式:
用来区分环境的关键字
    不同环境打包逻辑不同,因此,需要区分

三种模式:
    development(自动优化打包速度,添加一些调试过程中的辅助)
    production(自动优化打包结果)
    none(运行最原始的打包,不做任何额外处理)

模块

模块
Webpack中,模块的概念比较宽泛(一切皆为模块)
    JS模块
    一段CSS
    一张图片
    一个字体文件
    ……
https://www.webpackjs.com/concepts/modules

依赖图

模块之间是相互依赖的。

Webpack入门

初始化项目:
    npm init --y
安装Webpack:
    npm install webpack webpack-cli -D
创建入口文件:
    src/index.js
执行打包(必须指定mode):
    webpack ./src/index.js --output-path ./dist --mode=development
    webpack ./src/index.js -o ./dist --mode=development

配置文件

配置文件是用来简化命令行选项的
默认的配置文件名称是webpack.config.js
    webpack.config.js是以CommonJS规范进行组织的.
    使用webpack的过程,大部分就是跟配置文件打交道的过程
配置详情:
    https://www.webpackjs.com/configuration
常用配置项:
    mode 模式
    entry 入口
    output 出口
    module 模块配置-不同类型文件的配置-loader配置
    plugins 插件
    devServer 开发服务器的配置
案例:
    // Webpck配置文件
    const {resolve} = require("path"); // 引入path模块内取当前目录绝对路径的方法
    module.exports = {
        // 打包模式
        // mode: "development", // 开发模式
        mode: "production", // 生产模式
        // 入口文件
        entry: "./src/index.js",
        // 出口配置
        output: {
            // 输出目录 (输出目录必须是绝对路径)
            path: resolve(__dirname,"dist"),
            // __dirname取当前js文件所在的目录,__filename取目录当前自己的文件
            // resolve是目录解析=__dirname +/dist
            filename: "main.js"
        },
        // 模块配置
        module: {
            rules: [
                // 指定多个配置规则
            ]
        },
        // 开发服务器
        devServer: {

        },
        // 插件配置
        plugins: [

        ]
    }

Webpack基础

打包CSS-基本逻辑

打包逻辑:
    打包LESS
    打包成独立的CSS文件
    添加样式前缀
    格式校验
    压缩CSS
非JS文件需要对应的loader
    css-loader将CSS转换为JS(将CSS输出到打包后的JS文件中)
    style-loader把包含CSS内容的JS代码,挂载到页面的<style>标签当中
引入css:
    import "./css/main.css"
安装:
    npm i css-loader style-loader -D
配置:
    匹配后缀名:
        test: /\.css$/i
    指定加载器:
        use["style-loader","css-loader"]
Loader执行顺序:
    先右后左,先下后上
代码案例
module: {
    rules: [
        // 指定多个配置规则
        {
            test: /\.css$/i,
            // use中的loader的加载顺序: 先下后上
            use: [
                // 将JS中的样式挂载到style标签中
                "style-loader",
                // css-loader 按照 CommonJS规范,将样式文件输出到JS中
                "css-loader"
            ]
        }
    ]
},

打包Less

引入less
    import "./css/main.less"
安装
    npm install less less-loader -D
配置:
    匹配后缀名:
    test:/\.less$/i,
    指定加载器:
    use: ["style-loader","css-loader","less-loader"]

打包独立文件CSS

安装插件:
npm install mini-css-extract-plugin -D
引入插件:
const MiniCssExtraPlugin = require('mini-css-extract-plugin');

替换style-loader (use["MiniCssExtractPlugin.loader","css-loader"])
MiniCssExtractPlugin.loader: 将CSS打包到独立文件中

配置插件:
    new MiniCssExtractPlugin({
        filename: "css/[name].min.css"
    }) // 插件实例化

    // 如果如要压缩一行配置,自行百度

CSS添加兼容样式前缀

安装:
    npm install postcss-loader autoprefixer -D
配置:webpack.config.js,module,rules
    use: ['MiniCssExtractPlugin.loader','css-loader','postcss-loader']
新建:postcss.config.js
    module.exports = {
        "plugins": [
            require('autoprefixer')
        ]
    }
配置需要兼容的浏览器
    package.json中添加成员:browserslist

    "browerslist": [
        "last 1 version",
        "> 1%"
    ]

CSS格式校验

安装:
npm install stylelint stylelint-config-standard stylelint-webpack-plugin -D

引入:
webpack.config.js内引入
const StylelintPlugin = require('stylelint-webpack-plugin');

配置:
webpack.config.js,plugins内添加实例化对象.
new StylelintPlugin({
    // 指定需要格式校验的文件
    files: ["dist/css/*.css"]
})

指定校验规则:
    在package.json中指定stylelint
    "stylelint": {"extends": "stylelint-config-standard"}
    "stylelint": {
        "extends": "stylelint-config-standard",
        "rules": {

        }
    }

styleint:
https://stylelint.io
校验规则
    number-leading-zero
    line-height: .5;
    line-height: 0.5

stylelint-config-standard 规则集
https://github.com/stylelint/stylelint-config-standard

webpack内的standard规则集:
https://webpack.docschina.org/plugins/stylelint-webpack-plugin

打包CSS-压缩

安装
npm install optimize-css-assets-webpack-plugin -D --legacy-peer-deps

引入
webpack.config.js内
OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

配置
webpack.config.js的plugins内实例化
new OptimizeCssAssetsPlugin();

打包HTML

html-webpack-plugin
生成HTML文件(用于服务器访问),并在HTML中加载所有的打包资源
指定html模板,设置HTML变量,压缩HTML

安装:
    npm install html-webpack-plugin -D

在webpack.config.js内引入模块
在plugins内建立实例对象
    new HtmlWbapckPlugin(
        {
            // 指定打包后的文件名称
            filename: 'index.html',
            // 用来指定生成HTML的模板
            template: './src/index.ejs',
            // 指定HTML中变量
            title: '小雪花',
            // 设置转换规则
            minify: {
                // 设置HTML压缩规则
                collapseWhitespace: true,
                keepClosingSlash: true,
                removeComments: true,
                removeScriptTypeAttributes: true,
                removeRedundantAttributes: true,
                removeStyleLinkTypeAttributes: true,
                useShortDoctype: true
            }
        }
    )

打包JS-转义ES5

Webpack编译JS
目的:
将ES6+转成E5S,从而保证,JS在低版本浏览器的兼容性

安装:
npm install babel-loader @babel/core @babel/preset-env -D

配置:
https://www.npmjs.com/package/babel-loader

babel-preset-env(所有最新转换规则)

在webpack.config.js中module内配置规则:
{
    test: /\.m?js$/i,
    exclude: /node_modules/, //排除的目录
    use: {
        loader: "babel-loader",
        options: {
            presets: [
                ["@babel/preset-env",{targets: "defaults"}]
            ]
        }
    }
} // JS打包

Promise转换问题

@babel/preset-env只能转译基本语法(promise就不能转换)

@babel/polyfill(转译所有JS新语法)
用法:
安装:
npm install @babel/polyfill -D
在入口文件(index.js)中引入:
import '@babel/polyfill'
core-js代码配置
<!-- 使用core-js,需要关闭polyfill在入口文件的引入 -->
core-js(按需转译JS新语法)
安装: npm install core-js -D
配置:
    useBuiltIns: 'usage'
    指定版本: 3
{
    test: /\.m?js$/i,
    exclude: /node_modules/, //排除的目录
    use: {
        loader: "babel-loader",
        options: {
            presets: [
                [
                    "@babel/preset-env",
                    {
                        useBuiltIns: 'usage',
                        corejs: 3,
                        targets: {
                            chrome: '58',
                            ie: "9",
                            firefox: "60",
                            safari: "10",
                            edge: "17"
                        } //运行环境
                    }
                ]
            ]
        }
    }
} // JS打包

打包JS-格式校验

安装:
npm i eslint eslint-config-airbnb-base eslint-webpack-plugin eslint-plugin-import -D

eslint (校验JS代码格式的工具)

eslint-config-airbnb-base (最流行的JS代码格式规范)


配置:
webpack.json.js内引入
const ESLintPlugin = require('eslint-webpack-plugin');
plugins中实例化
new ESlintPlugin({
    // 自动解决常规的代码格式报错
    fix: true
})
package.json内添加规则信息:
"eslintConfig": {"extends": "airbnb-base"}

有些BOM、DOM内合理代码出现未定义或其他报错时,
可以在代码的上面添加注释:
    // eslint-disable-next-line

打包图片

file-loader
将用到的图片复制到输出目录,过滤不用的图片

安装:
npm install file-loader -D

url-loader
是file-loader的升级版,如果图片小于配置大小,会转成base64字符串
转成base64字符串后,图片会和js一起加载(减少图片请求次数)
安装:
npm install url-loader -D

webpack.config.js内module规则配置
{
    test: /\.(png|gif|jpe|jpg)$/i,
    use: {
        loader: "file-loader",
        options: {
            // 指定图片大小,小于该数值的图片会被转换为base64
            limit: 2*1024,
            // 图片输出路径与名字
            name: "./images/[name].[ext]"
        }
    }
}, // 处理图片

HTML处理HTML路径问题

安装:
npm install html-loader -D

webpack打包字体

file-loader
test: /\.(eot|svg|ttf|woff|woff2)$/i

copy-webpack-plugin
不需要处理的文件,可以直接复制到输出目录
https://www.npmjs.com/package/copy-webpack-plugin

clean-webpack-plugin
不需要处理的其他文件,可以直接复制到输出目录


const CopyWebpackPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 直接将 src 下,不需要特殊处理的文件,直接复制到输出目录中
new CopyWebpackPlugin({
    patterns: [
      {
        from: "src/public",
        to: "public"
      }
    ]
}),

  // 打包之前,先删除历史文件
new CleanWebpackPlugin(),

资源模块

功能:
    资源模块时一种模块类型,它允许使用资源文件,而无需配置额外的loader
    资源文件: 字体、图片、图标、html
    不用file-loader、url-loader也能加载图片和字体

webpack4处理方式:
    raw-loader、file-loader、url-loader

webpack5处理方式:
    asset/resource 发送一个单独的文件并导出URL,之前通过file-loader实现
    asset/inline 导出一个自愿的data URI 之前通过url-loader
    asset/source    导出资源的源代码(之前通过raw-loader实现)
    asset   在导出一个data URI和发送一个单独的文件之间自动选择

asset打包字体

// 匹配字体文件
{
    test: /\.(eot|svg|ttf|woff|woff2)$/i,
    // use: {
    //   loader: 'file-loader',
    //   options: {
    //     name: 'fonts/[name].[ext]'
    //   }
    // }

    // 使用资源模块处理字体文件
    // asset 可以在 asset/resource 和 asset/inline 之间进行选择
    // 如果文件小于 8kb,则使用 asset/inline 类型
    // 如果文件大于 8kb,则使用 asset/resource 类型
    type: 'asset',
    parser: {
      dataUrlCondition: {
        maxSize: 8 * 1024
      }
    },
    generator: {
      filename: "fonts/[name][ext]"
    }
},

asset打包图片

type: 'asset',
    parser: {
      dataUrlCondition: {
        maxSize: 8 * 1024
      }
    },
    generator: {
      filename: "image/[name][ext]"
    }
},

Webpack Dev Server

作用:发布Web服务,提高开发效率
使用:
webpack5 webpack serve
安装:
    npm install -D webpack-dev-server
// 开发服务器

Dev Server配置

devServer: {
    // 指定加载内容的路径
    contentBase: resolve(__dirname, 'output'),
    // 启用 gzip 压缩
    compress: true,
    // 端口号
    port: 9200,
    // 启动自动更新(禁用 hot)
    liveReload: true,
    // 配置代理:解决接口跨域问题
    proxy: {
        // http://localhost:9200/api
        '/api': {
            // http://localhost:9200/api/users => https://api.github.com/api/users
            target: 'https://api.github.com',
            // http://localhost:9200/api/users => https://api.github.com/users
            pathRewrite: {
            '^/api': ""
            },
            // 不能使用 localhost:9200 作为 github 的主机名
            changeOrigin: true
        }
    }
},
// 配置目标
target: "web",

区分打包环境

webpack区分环境打包

通过环境变量区分: --env.production
通过webpack.config.js中判断ENV
module.exports = (env,argv)=>{
    if(env.production) {
        // 生产环境配置
    } else {
        // 开发环境配置
    }
};

通过配置文件区分:
webpack.dev.conf.js
webpack.prod.conf.js
执行打包时,指定配置文件
webpack --config webpack.[dev|prod].conf.js

通过环境变量区分环境

webpack5:
设置环境变量:
webpack --env production

根据环境变量不同,作出不同打包模式
// 判断当前是否是生产环境打包
if(env.production) {
    config.mode = "production";
    config.plugins = [
    ];
    return config;
}

通过配置文件进行区分

webpack.dev.conf.js
webpack.prod.conf.js
webpack.base.conf.js (公共配置)

webpack.merge
安装: npm install webpack-merge
将多个配置合并在一起

webpack --config webpack.[dev|prod].conf.js
开发模式打包:
webpack.edv.conf.js
const {merge} = require('webpack-merge');
const baseWebpackConfig = require("./webpack.base.conf.js");
const devWebpackConfig = merge(baseWebpackConfig{
    // 这里开发模式对应配置
});
module.exports = devWebpackConfig;
webpack --config webpack.dev.conf.js
生产模式打包:
webpack.prod.conf.js
const {merge} = require('webpack-merge');
const baseWebpackConfig = require("./webpack.base.conf.js");
const devWebpackConfig = merge(baseWebpackConfig{
    // 这里生产模式对应配置
});
module.exports = devWebpackConfig;
webpack --config webpack.prod.conf.js
npmscript简化
scripts: {
    "buid:dev": "webpack --config webpack.dev.conf.js",
    "buid:prod": "webpack --config webpack.prod.conf.js",
    "dev": "webpack serve --config webpack.dev.conf.js",
    "start": "webpack serve --config webpack.pord.conf.js"
}

DefinePlugin

为配置注入全局变量
开发环境和生产环境的接口地址不同
代码案例
const webpack = require("webpack");
module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            // 变量值要求的是一个代码片段
            API_BASE_URL: JSON.stringfy("http://.....")
        })
    ]
};

自定义plugin

webpack插件是一个具有apply方法的JavaScript对象,apply方法
会被webpack compiler代用,并且在整个编译声明周期都可以访问
compiler对象。

原理:
    通过再生命周期的钩子中挂载函数,来实现功能扩展

声明周期

声明周期:
声明周期就是整个声明过程中的关键节点

人: 出生->入学->毕业->结婚->生子->死亡
程序: 初始化->挂载->渲染->渲染->展示->销毁

钩子

钩子是提前在可能增加功能的地方,预设的一个函数
生命周期中的函数

钩子当中最开始可能没有功能,但是保留的有挂载功能的
一些能力。

webpack常用钩子

https://www.webpackjs.com

钩子    描述    类型
environment 环境准备好 SycnHook
compile 编译开始 SyncHook
compilation 编译开始 SyncHook
emit 打包资源到output之前 AsyncSeriesHook
afterEmit 打包资源到output AsyncSeriesHook
done 打包完成 SyncHook

自定义plugin的语法

// 声明自定义插件
class MyPlugin {
constructor(options) {
    console.log('插件配置选项', options)
    this.userOptions = options || {}
}

// 必须声明 apply 方法
apply(compiler) {
    // 在钩子上挂载功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
    // compilation 是此次打包的上下文
    for (const name in compilation.assets) {
        console.log(name)
        // 针对 css 文件,执行相关操作
        // if (name.endsWith('.css')) {
        if (name.endsWith(this.userOptions.target)) {
        // 获取处理之前的内容
        const contents = compilation.assets[name].source()
        // 将原来的内容,通过正则表达式,删除注释
        const noComments = contents.replace(/\/\*[\s\S]*?\*\//g, '')
        // 将处理后的结果,替换掉
        compilation.assets[name] = {
            source: () => noComments,
            size: () => noComments.length
        }
        }
    }
    })
}
}
module.exports = MyPlugin


配置文件内引入:
const MyPlugin = require("./plugin/my-plugin");
module.exports = {
    plugins: [
        new MyPlugin({
            // 插件选项
        })
    ]
};

自定义loader

Loader本质上就是一个EMS模块,它导出函数,在函数中对打包资源
进行转换。

声明一个读取markdown (.md) 文件内容的loader
marked (将markdown语法转换成html)

loader-utils (接受loader的配置项)
npm install marked loader-utils -D

多入口打包

代码分离

如果把所有代码都打包到一起,可能最终代码非常大,从而影响
加载事件。
而且,很多代码初始是不需要的,因此我们可以根据代码使用紧急程度,
将代码分割打包后,按需加载。

多入口打包:
配置entry加载多个入口文件
提取公用模块:
optimization.splitChunks.chunks: all
动态导入: 按需加载 | 预加载
多入口打包
entry (后面写成对象)
{index: './src/index.js',about: './src/about.js'}

output.filename (不能写成固定名称,否则报错)
[name].bundle.js

new HtmlWebpackPlugin (不同页面加载各自的bundle)
chunks: ['index']
chunks: ['about']
代码案例
new HtmlWebpackPlugin({
    // 指定打包后的文件名称
    filename: 'about.html',
    // 用来指定,生成 HTML 的模板
    template: './src/index.ejs',
    // 指定 HTML 中使用的变量
    title: "关于我们",
    chunks ['index'],
    minify: {
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true
    }
}),
提取公共文件
如果多个页面都用到了一个公共文件,每个页面都将公共文件打包一次
是不合理的。更好的办法是将公共文件提取出来。

optimization.splitChunks.chunks: 'all'
将公共文件提取出来,单独打包
在webpack.config.js内进行配置
optimization: {
    splitChunks: {
        chunks: 'all'
    }
}
动态导入
懒加载:
默认不加载,事件触发后才加载
webpackChunkName: '加载名称'

预加载:
先等待其他资源加载,浏览器空闲时,再加载
webpackPrefetch: true
缺点: 在移动端有兼容问题
懒加载代码案例
// 入口文件设置懒加载
document.getElementById('btn').onclick = function() {
    // import 启动懒加载
    // webpackChunkName: 'desc' 指定懒加载的文件名称
    // webpackPrefetch: true 启动预加载
    import(/* webpackChunkName: 'desc', webpackPrefetch: true */'./wp').then(({ desc }) => {
        alert(desc())
    })
}

懒加载调用文件(wp.js):
console.log('模块加载了')
export desc()=> {
    return "Webpack 是一款前端打包工具"
}

源码映射Source Map

什么是Soure Map
是一种源代码与构建后代码之间的映射技术
通过.map,将构建后的代码与源代码之间建立映射关系。

为什么要用Source Map
问题: 构建后的代码,出了问题之后不好定位。
方案: 有了Source Map后,可以快速定位问题代码

如何生成Source Map
devtool: '映射模式'

config.devtool = 'source-map' // 默认行列映射(一般开发模式调试用)
config.devtool = 'hidden-source-map' // 映射出来没注释,不能定位问题(一般发布生产模式服务用)

映射模式

不同的映射模式的报错定位效果和打包执行速度是不同的。
如何选区合适的映射模式:

开发环境:
    eval-cheap-module-source-map
生产环境:
    none | nosources-source-map

Tree Shaking删除死代码

作用

Treee Shaking的作用是删除未引用的代码
    return后面的代码
    只声明,而未使用的函数
    只引入,未使用的代码
比如引入了Jquery,但是有很多代码没有用到。Tree Shaking会自动删除.

前提

使用ES Moudules规范的模块,只能在执行Tree Shaking
Trese Shaking 依赖于ES Modules的静态语法分析

如何使用

生产模式: Tree Shaking会自动开启

开发模式: usedExports
optimization.usedExports(标记没用的代码)
/* unused harmony export xxxx */

terser-webpack-plugin(删除没用的代码)
optimization-minimize: true (删除unused harmony export xxx标记的代码)

兼容问题

Tree Shaking与Source Map存在兼容性问题
只能使用以下几种模式:
source-map | inline-source-map | hidden-source-map | nosources-source-map

总结

Tree Shaking 删除未使用,且无副作用的代码
开启副作用:
optimization.sideEffects: true
标识代码是否有副作用

表示代码是否有副作用:
"sideEffects":
    false 所有的代码都没有副作用,告诉webpack可以安全的删除未用的exports
    true 所有代码都有副作用
    数组 告诉webpack哪些模块有副作用不删除
    ['./src/wp.js','*.css']

缓存

Babel缓存

Babel缓存
cacheDirectory: true (第二次构建时,会读取之前的缓存)

文件资源换缓存
如果代码在缓存期内,代码更新后看不到实际效果
方案: 将代码文件名称,设置为哈希名称,名称发生变化时就加载最新内容

Webpack哈希值
hash (每次Webpack打包生成的hash值)
chunkhash (不同chunk的hash值不同-同一次打包可能生成不同的chunk)
contenthash (不同内容的hash值不同-同一个chunk中可能有不同的内容)

    {
      test: /\.m?js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,

模块解析

resolve: 配置模块解析规则
    alias: 配置模块加载的路径别名
    alias: {"@": resolve('src')}
    extensions: 引入模块时,客户以省略哪些后缀
    extensions: [".js",".json"]
    // 模块的解析规则
resolve: {
  alias: {
    // 指定路径的别名
    '@': resolve('src')
  },
  // 指定引入文件的后缀名(指定之后,在引入文件时,后缀名可以省略)
  extensions: ['.js', '.json', '.less'],
  // 指定模块默认加载的路径
  modules: [resolve(__dirname, './node_modules'), 'node_modules']
},

排除依赖

externals
排除打包依赖项(防止对某个依赖项进行打包)
一般来说,一些成熟的第三方库,是不需要打包的.
例如: jQuery,我们可以在模板文件中直接引入CDN中的压缩。

// 排除打包依赖项
externals: {
  'jquery': 'jQuery'
},

模块联邦

多个应用,可以共享一个模块(本地可以调用远程的模块)4
模块提供方
    name: 当前应用名称(供调用方使用)
    filename: 打包后的文件名称(供调用方使用)
    exposes: 暴露模块(相当于export导出)
        模块名称: 模块文件路径
模块使用方
    remote: 导入模块(相当于import)
        导入后的别名: "远程应用名称@远程地址/远程导出的文件名"
    import("导入后的别名/模块名称").then(//...)

引入模块联邦

const Mfp = require('webpack').container.ModuleFederationPlugin;
plugins
导出:
new Mfp({
  // 应用名称(供调用方使用)
  name: 'app1',
  // 调用方引入的文件名称
  filename: 'app1.js',
  // 暴露模块
  exposes: {
    // 模块名称: 模块对应的代码路径
    './Sitename': './src/Sitename.js'
  }
})
导入:
new Mfp({
    // 导入模块
    remotes: {
     // 导入别名:“远程应用名称@远程应用地址/远程导出文件的名称”
     appone: "app1@http://localhost:3001/app1.js"
   }
 })

项目部署

常规操作

npm i -D webpack webpack-cli html-webpack-plugin webpack-dev-server copy-webpack-plugin clean-webpack-plugin mini-css-extract-plugin css-loader style-loader postcss-loader autoprefixer babel-loader @babel/core @babel/preset-env

配置文件

webpack.config.js
src:
    index.js
    index.ejs